Passed
Pull Request — filestream (#169)
by
unknown
05:09
created

file-write.ts ➔ streamOriginalIntoNewFileSync   A

Complexity

Conditions 3

Size

Total Lines 10
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 10
rs 9.95
c 0
b 0
f 0
cc 3
1
import {
2
    fsWritePromise,
3
    processFileSync,
4
    processFileAsync,
5
    fsExistsPromise,
6
    fsWriteFilePromise,
7
    fsRenamePromise,
8
    unlinkIfExistSync,
9
    unlinkIfExist,
10
    fsReadAsync
11
} from "./util-file"
12
import * as fs from 'fs'
13
import { Header, getId3Tag } from "./id3-tag"
14
import { WriteOptions } from "./types/write"
15
import { hrtime } from "process"
16
17
// Must be at least Header.size which is the min size to detect an ID3 header.
18
// Naming it help identifying the code handling it.
19
const RolloverBufferSize = Header.size
20
21
const MinBufferSize = RolloverBufferSize + 1
22
const DefaultFileBufferSize = RolloverBufferSize + 20 * 1024 * 1024
23
24
export function writeId3TagToFileSync(
25
    filepath: string,
26
    id3Tag: Buffer,
27
    options: WriteOptions
28
): void {
29
    if (!fs.existsSync(filepath)) {
30
        fs.writeFileSync(filepath, id3Tag)
31
        return
32
    }
33
    const tempFilepath = makeTempFilepath(filepath)
34
    processFileSync(filepath, 'r', (readFileDescriptor) => {
35
        try {
36
            processFileSync(tempFilepath, 'w', (writeFileDescriptor) => {
37
                fs.writeSync(writeFileDescriptor, id3Tag)
38
                copyFileWithoutId3TagSync(
39
                    readFileDescriptor,
40
                    writeFileDescriptor,
41
                    getFileBufferSize(options)
42
                )
43
            })
44
        } catch(error) {
45
            unlinkIfExistSync(tempFilepath)
46
            throw error
47
        }
48
    })
49
    fs.renameSync(tempFilepath, filepath)
50
}
51
52
export async function writeId3TagToFileAsync(
53
    filepath: string,
54
    id3Tag: Buffer,
55
    options: WriteOptions
56
): Promise<void> {
57
    if (!await fsExistsPromise(filepath)) {
58
        await fsWriteFilePromise(filepath, id3Tag)
59
        return
60
    }
61
    const tempFilepath = makeTempFilepath(filepath)
62
    await processFileAsync(filepath, 'r', async (readFileDescriptor) => {
63
        try {
64
            await processFileAsync(tempFilepath, 'w',
65
                async (writeFileDescriptor) => {
66
                    await fsWritePromise(writeFileDescriptor, id3Tag)
67
                    await copyFileWithoutId3TagAsync(
68
                        readFileDescriptor,
69
                        writeFileDescriptor,
70
                        getFileBufferSize(options)
71
                    )
72
                }
73
            )
74
        } catch(error) {
75
            await unlinkIfExist(tempFilepath)
76
            throw error
77
        }
78
79
    })
80
    await fsRenamePromise(tempFilepath, filepath)
81
}
82
83
function getFileBufferSize(options: WriteOptions) {
84
    return Math.max(
85
        options.fileBufferSize ?? DefaultFileBufferSize,
86
        MinBufferSize
87
    )
88
}
89
90
function makeTempFilepath(filepath: string) {
91
    // A high-resolution time is required to avoid potential conflicts
92
    // when running multiple tests in parallel for example.
93
    // Date.now() resolution is too low.
94
    return `${filepath}.tmp-${hrtime.bigint()}`
95
}
96
97
class Id3TagRemover {
98
    buffer: Buffer
99
    rolloverSize = 0
100
    continue = false
101
102
    constructor(bufferSize: number) {
103
        // TODO enforce min buffer size here,
104
        // i.e. bufferSize + RolloverBufferSize + 1
105
        this.buffer = Buffer.alloc(bufferSize)
106
    }
107
108
    getReadBuffer() {
109
        return this.buffer.subarray(this.rolloverSize)
110
    }
111
112
    processReadBuffer(readSize: number) {
113
        let data = this.buffer.subarray(0, this.rolloverSize + readSize)
114
115
        // TODO extract that to id3-tag
116
        // Remove tags from `data`
117
        let missingBytes = 0
118
        const parts = Array.from((function*() {
119
            let tag
120
            while((tag = getId3Tag(data))) {
121
                yield tag.before
122
                data = tag.after
123
                missingBytes = tag.missingBytes
124
            }
125
        })())
126
127
        // Exclude rollover window on the last part
128
        this.rolloverSize = Math.min(RolloverBufferSize, data.length, readSize)
129
        const rolloverStart = data.length - this.rolloverSize
130
        const rolloverData = Buffer.from(data.subarray(rolloverStart))
131
        parts.push(data.subarray(0, rolloverStart))
132
133
        const writeBuffer = Buffer.concat(parts)
134
135
        // Update rollover window
136
        rolloverData.copy(this.buffer)
137
138
        this.continue = this.rolloverSize !==0 || missingBytes !== 0
139
140
        return {
141
            skipBuffer: Buffer.alloc(missingBytes),
142
            writeBuffer
143
        }
144
    }
145
}
146
147
function copyFileWithoutId3TagSync(
148
    readFileDescriptor: number,
149
    writeFileDescriptor: number,
150
    fileBufferSize: number
151
) {
152
    const remover = new Id3TagRemover(fileBufferSize)
153
    do {
154
        const readBuffer = remover.getReadBuffer()
155
        const sizeRead = fs.readSync(readFileDescriptor, readBuffer)
156
        const { skipBuffer, writeBuffer } = remover.processReadBuffer(sizeRead)
157
        fs.readSync(readFileDescriptor, skipBuffer)
158
        fs.writeSync(writeFileDescriptor, writeBuffer)
159
    } while(remover.continue)
160
}
161
162
async function copyFileWithoutId3TagAsync(
163
    readFileDescriptor: number,
164
    writeFileDescriptor: number,
165
    fileBufferSize: number
166
) {
167
    const remover = new Id3TagRemover(fileBufferSize)
168
    do {
169
        const readBuffer = remover.getReadBuffer()
170
        const sizeRead = await fsReadAsync(readFileDescriptor, readBuffer)
171
        const { skipBuffer, writeBuffer } = remover.processReadBuffer(sizeRead)
172
        await fsReadAsync(readFileDescriptor, skipBuffer)
173
        await fsWriteFilePromise(writeFileDescriptor, writeBuffer)
174
    } while(remover.continue)
175
}
176